package server;

import java.io.IOException;
import java.util.LinkedList;
import java.util.Queue;

import net.ByteBuffer;
import net.ByteReader;
import net.Connection;
import net.Packet;
import net.Protocol;
import util.Clock;
import util.Logger;
import core.Entity;
import core.GameEngine;
import core.online.CommandBank;
import core.online.Player;

/**
 * All clients which connect to this type of server stay connected throughout
 * the entire game. This server manages a synchronized game-state for all
 * clients.
 * 
 * @author Jason
 * 
 */
public abstract class GameHostingServer extends Server {

	protected int COMMAND_DELAY = 2; // delay 2 frames

	private Thread gameLoop;
	private final String controllerIP;

	// a list of clients that will be connecting
	private final LinkedList<String> clientsToAccept = new LinkedList<String>();
	private boolean controllerConnected = false;
	protected LinkedList<Player> connectedClients = new LinkedList<Player>();
	protected GameEngine serverEngine = new GameEngine();
	protected CommandBank commandBank = new CommandBank();
	protected Thread latencyThread = null;

	protected long lastTime; // the time of the last processed turn
	protected int lastTurn = -1; // the last processed turn
	private int updatesPerSecond = 20; // normally 20
	private int milliPerUpdate = 1000 / updatesPerSecond;
	private double secondsPerUpdate = 1.0 / updatesPerSecond;

	public GameHostingServer(String controllerIP, int tcpPort, int udpPort)
			throws IOException {
		super(tcpPort, udpPort);
		this.controllerIP = controllerIP;
		Logger.log(Logger.FINE, "Game hosting server started on ports "
				+ tcpPort + " and " + udpPort);
	}

	@Override
	protected synchronized void connectionReceived(Connection conn) {
		if (!controllerConnected && conn.getIP().equals(controllerIP)) {
			Logger.log(Logger.FINE,
					"Controller connected to GameHosting Server");
			controllerConnected = true;
		} else if (clientsToAccept.remove(conn.getIP())) {
			Logger.log(Logger.NORMAL,
					"Client connected to GameHosting Server from IP "
							+ conn.getIP());
			connectedClients.add(createPlayer(conn));
			// tell the client to open up a UDP Socket
			activateUDP(conn);
		} else {
			if (conn.getIP().equals("127.0.0.1")) {
				clientsToAccept.remove(0);
				connectedClients.add(createPlayer(conn));
				activateUDP(conn);
				System.out.println("got a localhost");
				return;
			}
			Logger.log(Logger.WARNING,
					"Unauthorized connection to GameHosting Server:" + " IP="
							+ conn.getIP());
		}
	}

	private void startWhenReady() {
		Thread t = new Thread(new Runnable() {
			public void run() {
				while (true) {
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
					}
					if (readyToStart()) {
						Logger.log(Logger.FINE,
								"All clients connected, initializing...");
						stopServer(false);
						startLatencyThread();
						initializeGame();
						// tell the clients to start their engines
						for (Player p : connectedClients) {
							p.conn.send(START_GAME);
						}
						startGameLoop();
						break;
					}
				}
			}
		});
		t.setDaemon(true);
		t.start();
	}

	protected synchronized boolean readyToStart() {
		if (clientsToAccept.isEmpty()) {
			for (Player player : connectedClients) {
				if (player.conn.getUDPOutbound() <= 0) {
					return false;
				}
			}
			return true;
		}
		return false;
	}

	@Override
	public void receive(byte[] bytes, Connection conn) {
		if (gameLoop == null) {
			String msg = new String(bytes);
			String[] m = msg.split(DELIM);
			String command = m[0];
			if (command.equals(MAKE_READY)) {
				for (int i = 1; i < m.length; i++) {
					clientsToAccept.add(m[i]);
				}
				Logger.log(Logger.NORMAL, "Ready for clients to join:");
				for (int i = 1; i < m.length; i++) {
					Logger.log(Logger.NORMAL, m[i]);
				}
				startWhenReady();
				conn.send(MAKE_READY);
			} else {
				Logger.log(Logger.WARNING, "Dont recognize message: " + msg);
			}
		} else {
			// we are in the game
			if (bytes[0] == Protocol.PING_UDP) {
				long currentTime = Clock.getTime();
				ByteReader reader = new ByteReader(bytes);
				reader.read();
				long sentTime = reader.readLong();
				int elapsedMillis = (int) (currentTime - sentTime);
				Logger.log(Logger.NORMAL, "UDP pingtime: " + elapsedMillis
						+ " millis.");
			} else if (bytes[0] == Protocol.PING_TCP) {
				long currentTime = Clock.getTime();
				ByteReader reader = new ByteReader(bytes);
				reader.read();
				long sentTime = reader.readLong();
				int elapsedMillis = (int) (currentTime - sentTime);
				Logger.log(Logger.NORMAL, "TCP pingtime: " + elapsedMillis
						+ " millis.");
			} else {
				synchronized (commandBank) {
					commandBank.add(new Packet(conn, bytes), lastTurn
							+ COMMAND_DELAY);
				}
			}
		}
	}

	@Override
	public void tcpConnectionBroken(Connection conn) {
		if (connectedClients.remove(conn)) {
			Logger.log(Logger.NORMAL, "A client disconnected.");
			if (connectedClients.size() == 0) {
				Logger.log(Logger.NORMAL,
						"All clients disconnected, stopping server.");
				gameLoop = null;
				super.stopServer(true);
			}
		}
		super.tcpConnectionBroken(conn);
	}

	protected void startGameLoop() {
		gameLoop = new Thread(new Runnable() {
			public void run() {
				lastTime = Clock.getTime();
				while (gameLoop != null) {
					long currentTime = Clock.getTime();
					int elapsed = (int) (currentTime - lastTime);
					int numUpdateCycles = elapsed / milliPerUpdate;
					int currentTurn = lastTurn;
					for (int i = 0; i < numUpdateCycles; i++) {
						currentTurn++;
						Queue<Packet> packets = commandBank
								.getCommandsFor(currentTurn);
						while (!packets.isEmpty()) {
							readPacket(packets.poll());
						}
						serverEngine.tick(secondsPerUpdate);
						for (Player p : connectedClients) {
							p.getEngine().tick(secondsPerUpdate);
						}
						syncEngines();
						lastTurn = currentTurn;
						lastTime += milliPerUpdate;
					}
				}
			}
		});
		gameLoop.start();
	}

	private final LinkedList<Entity> entitiesChanged = new LinkedList<Entity>();
	private final ByteBuffer packetBuffer = new ByteBuffer(256);
	private final ByteBuffer entityBuffer = new ByteBuffer(256);

	/**
	 * Synchronizes the client engines with the server engine. This method will
	 * send out packets if necessary.
	 */
	protected void syncEngines() {
		int numEntitiesChanged = 0;
		for (Player player : connectedClients) {
			serverEngine.synchronize(player.getEngine(), entitiesChanged);
			numEntitiesChanged += entitiesChanged.size();
			while (entitiesChanged.isEmpty() == false) {
				Entity changed = entitiesChanged.poll();
				changed.writeEntity(entityBuffer);
				if (entityBuffer.size() > 256)
					throw new RuntimeException("Size of entity is too large!");
				if (packetBuffer.size() + entityBuffer.size() > 256) {
					player.conn.sendUDP(packetBuffer.getBuffer(), 0,
							packetBuffer.size());
					packetBuffer.reset();
				}
				packetBuffer.add(entityBuffer);
				entityBuffer.reset();
			}
			if (packetBuffer.size() > 0) {
				player.conn.sendUDP(packetBuffer.getBuffer(), 0, packetBuffer
						.size());
				packetBuffer.reset();
			}
		}
		if (numEntitiesChanged > 0)
			Logger.log(Logger.NORMAL, numEntitiesChanged
					+ " entity updates sent to clients.");
	}

	protected Player createPlayer(Connection conn) {
		return new Player(conn);
	}

	protected final void setUpdatesPerSecond(int amount) {
		updatesPerSecond = amount;
		milliPerUpdate = 1000 / updatesPerSecond;
		secondsPerUpdate = 1.0 / updatesPerSecond;
	}

	protected void startLatencyThread() {
		latencyThread = new Thread(new Runnable() {
			public void run() {
				ByteBuffer bb = new ByteBuffer();
				while (latencyThread != null) {
					synchronized (connectedClients) {
						for (Player p : connectedClients) {
							long time = Clock.getTime();
							bb.add(PING_UDP);
							bb.addLong(time);
							byte[] bytes = bb.toByteArray();
							p.conn.sendUDP(bytes);
							bytes[0] = PING_TCP;
							p.conn.sendTCP(bytes);
							bb.reset();
						}
					}
					try {
						Thread.sleep(4000);
					} catch (InterruptedException e) {
					}
				}
			}
		});
		latencyThread.setDaemon(true);
		latencyThread.start();
	}

	/**
	 * Called once all clients are fully connected.
	 */
	protected abstract void initializeGame();

	protected abstract void readPacket(Packet packet);
}
